# -*- coding: utf-8 -*-

# This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

__all__ = ['Container', 'ErrContainerFieldsEmpty']

import copy

from _pylot.minimal.datum import Datum
from _pylot.minimal.utils import (check_var_naming_convention,
  check_type, ENCODING, error, make_alias)
from _pylot.minimal.pylotdict import PyLotDict
from _pylot.minimal.exception import CustomException

class ErrContainerFieldsEmpty(CustomException):
  title = u'ErrContainerFieldsEmpty'
  message = u'Container contains empty fields'
  desc = u'Container is not valid because it contains empty fields (value=None)'

class Container(PyLotDict):
  u"""
  An improved `dict` designed for Datum handling.

  >>> from pyLot.core import Container, Datum
  >>> book = Container(u'book', data=[Datum(u'd1', value=1, group=u'g1')])
  >>> book = Container(u'book', data=[Datum(u'one', int, 1), Datum(u'two', unicode, u'deux')])
  >>> book.keys()
  [u'one', u'two']
  """

  ordered_keys = (u'name', u'nature', u'tooltip', u'group', u'alias')

  def __init__(self, name=None, nature=None, tooltip=None, group=None, alias=None,
               data=None, datum_type=Datum):
    PyLotDict.__init__(self)
    self._datum_type = datum_type
    self._signal_methods = dict(
      natureChanged=set([]),
      tooltipChanged=set([]),
      )

    check_type(u'name', name, unicode, none=True)
    if data is None :
      data = []

    self._name = None
    self._tooltip = None
    self._group = None
    self._alias = None
    self._nature = None

    self.name = name
    self.tooltip = tooltip
    self.group = group
    self.alias = alias
    self.nature = nature


    for obj in data :
      self.add(obj)

  def __copy__(self):
    return self.__deepcopy__()

  def __deepcopy__(self, memo=None, _nil=[]):
    c = self.__class__(name=self.name, nature=self.nature, tooltip=self.tooltip,
                  group=self.group, alias=self.alias,
                  datum_type=self._datum_type)
    for k, v in self.iteritems() :
      c[k] = copy.deepcopy(v)
    return c

  def __str__(self, *args, **kwargs):
    return repr(self)

  def __repr__(self, *args, **kwargs):
    try:
      indent = kwargs[u'indent']
    except KeyError :
      indent = 1
    space = u'  ' * indent

    keys = []
    for k in self.ordered_keys :
      v = getattr(self, k)
      if v :
        keys.append(u'%s=%r' % (k, v))

    fields = u', '.join(keys)
    if fields :
      txt = u"%s(%s," % (type(self).__name__, fields)
    else :
      txt = u"%s(" % type(self).__name__
    if self :
      txt += u'\n%sdata=[\n' % space
      for datum_id, datum in self.iteritems():
        if isinstance(datum, Container):
          txt += u'  %s%s,\n' % (space, datum.__repr__(indent=indent + 2).decode(ENCODING))
        else :
          txt += u'  %s%s,\n' % (space, repr(datum).decode(ENCODING))
      txt += u'%s]' % space
    txt += u')'
    return txt.encode(ENCODING)

  def __call__(self, key):
    try :
      return self[key].value
    except AttributeError :  # key is a container instead of datum
      raise KeyError, key

  def _emitSignal(self, signal, *args, **kargs):
    for meth in self._signal_methods[signal] :
      meth(*args, **kargs)

  def connect(self, signal, method):
    self._signal_methods[signal].add(method)

  def disconnect(self, signal, method):
    if method in self._signal_methods[signal] :
      self._signal_methods[signal].remove(method)


  def find(self, key):
    try :
      return self[key]
    except KeyError :
      return None

  def _getname (self):
    return self._name

  def _setname(self, name):
    u"""
    checks if name complies with python namming conventions
    """
    if name is None :
      self._name = None
    else :
      name = name.strip()
      check_type(u'name', name, unicode)
      check_var_naming_convention(name)
      self._name = name

  def _getnature(self):
    return self._nature

  def _setnature(self, nature):
    u"""
    checks if name complies with python namming conventions
    """
    old_value = self._nature
    self._nature = nature
    if nature is not None :
      for datum in self.datumValues() :
        datum.nature = nature
      for collection in self.containerValues() :
        collection.nature = nature

    if old_value != self._nature:
      self._emitSignal('natureChanged', self, old_value, self._nature)

  def _gettooltip(self):
    return self._tooltip

  def _settooltip(self, tooltip):
    check_type(u'tooltip', tooltip, unicode, none=True)
    old_value = self._tooltip
    self._tooltip = tooltip
    if old_value != self._tooltip:
      self._emitSignal('tooltipChanged', self, old_value, self._tooltip)

  def _getalias(self):
    return self._alias

  def _setalias(self, alias):
    check_type(u'alias', alias, unicode, none=True)
    self._alias = alias

  def _getgroup(self):
    return self._group

  def _setgroup(self, group):
    if group is None :
      self._group = None
    else :
      group = group.strip()
      check_type(u'group', group, unicode)
      check_var_naming_convention(group)
      self._group = group

  # Datum's fields
  name = property(fget=_getname, fset=_setname, doc=u'Datum name (identifier)')
  tooltip = property(fget=_gettooltip, fset=_settooltip, doc=u'Tooltip')
  group = property(fget=_getgroup, fset=_setgroup, doc=u'Group')
  alias = property(fget=_getalias, fset=_setalias,
                   doc=u'alias, usually="human readable" name of Datum')
  nature = property(fget=_getnature, fset=_setnature)
  datum_type = property(fget=lambda self:self._datum_type)

  def getValue(self, path, default_value):
    try :
      return self[path]
    except KeyError :
      return default_value

  def setValue(self, path, value):
    self[path] = value

  def addDatum(self, datum):
    if isinstance(datum, Datum):
      if datum.name not in self and datum.name :
        self._checkDatumNature(datum)
        PyLotDict.__setitem__(self, datum.name, datum)
      elif datum.name :
        PyLotDict.__setitem__(self, datum.name, datum)
      else :
        raise ValueError, u'Only named datum can be appended to %s' % self.__class__.__name__
    else :
      check_type(u'datum', datum, Datum)

  def addContainer(self, collection):
    if isinstance(collection, Container):
      if collection.name not in self and collection.name :
        PyLotDict.__setitem__(self, collection.name, collection)
    else :
      check_type(u'collection', collection, Container)

  def add(self, obj):
    if isinstance(obj, Datum):
      self.addDatum(obj)
    elif isinstance(obj, Container):
      self.addContainer(obj)
    else :
      check_type(u'obj', obj, [Datum, Container])


  def _checkDatumNature(self, datum):
    u"""
    Checks if datum.nature is valid (if Container.nature is defined)
    
    :param datum: the datum you want to test
    :type datum: :class:`~pylot.core.Datum`
    """
    if self.nature :
      if not issubclass(datum.nature, self.nature) :
        raise TypeError, "type must be %s, (current type is %s)" % (
          self.nature, datum.nature)

  @classmethod
  def _checkDatumName (cls, key, datum):
    u"""
    Checks if key and datum name are identical.
    If doesn't match, a NameError exception is raised.
    """
    try :
      assert (datum.name == key)
    except AssertionError :
      raise NameError, error(u'key (%s) and datum name(%s) must be identical.' % (
        key, datum.name))


  def __setitem__(self, key, value):
    try :
      orig = self[key]
    except KeyError :
      check_type(u'key', key, unicode)
      paths = key.split(u'/')
      if key.startswith(u'/'):
        return self.__setitem__(u'/'.join(paths[1:]))
      if len(paths) == 1 :
        if isinstance(value, Container):
          self.addContainer(value)
        elif isinstance(value, Datum):
          self._checkDatumName(key, value)
          self.addDatum(value)
        elif self.nature:
          datum = self._datum_type(unicode(key), self.nature, value)
          self.addDatum(datum)
        else :
          check_type(u'value', value, (Container, Datum))
      else :
        try :
          container = self[paths[0]]
        except KeyError :
          container = self.__class__(paths[0], nature=self.nature)
          self.addContainer(container)

        container[u'/'.join(paths[1:])] = value

    else :
      if isinstance(orig, Datum) and not isinstance(value, (Datum, Container)):
        orig.value = value
      else :
        PyLotDict.__setitem__(self, key, value)

  def iterDatumKeys(self):
    for key, value in self.items() :
      if isinstance(value, Datum):
        yield key

  def iterContainerKeys(self):
    for key, value in self.items() :
      if isinstance(value, Container):
        yield key

  def iterDatumValues(self):
    for value in self.values() :
      if isinstance(value, Datum):
        yield value

  def iterContainerValues(self):
    for value in self.values() :
      if isinstance(value, Container):
        yield value

  def iterDatumItems(self):
    for key, value in self.items() :
      if isinstance(value, Datum):
        yield key, value

  def iterContainerItems(self):
    for key, value in self.items() :
      if isinstance(value, Container):
        yield key, value

  def datumValues(self):
    return [v for v in self.iterDatumValues()]

  def datumKeys(self):
    return [v for v in self.iterDatumKeys()]

  def datumItems(self):
    return [v for v in self.iterDatumItems()]

  def containerValues(self):
    return [v for v in self.iterContainerValues()]

  def containerKeys(self):
    return [v for v in self.iterContainerKeys()]

  def containerItems(self):
    return [v for v in self.iterContainerItems()]

  def toKeyValueDict(self):
    dic = {}
    for name, datum in self.iterDatumItems():
      dic[name] = datum.value
    for name, container in self.iterContainerItems():
      dic[name] = container.toKeyValueDict()
    return dic

  def resetDataToDefault(self):
    for datum in self.datumValues() :
      datum.value = datum.default_value
    for collection in self.containerValues() :
      collection.resetDataToDefault()

  def getDatum(self, key):
    datum = self[key]
    if not isinstance(datum, Datum):
      raise KeyError, key
    else :
      return datum

  @classmethod
  def display(cls, collection, level=0):
    print '-' * level, collection.name
    for v in collection._data.values() :
      print '-' * (level + 1), repr(v)
    for v in collection._containers.values() :
      cls.display(v, level + 1)


  def upFromDatum (self, datum, mod_attrs=None):
    if mod_attrs is None :
      mod_attrs = datum.getFieldsAll()
    self._up(datum, mod_attrs=mod_attrs)

  def _up(self, *t, **d):
    u"""
    This method updates book.
    See book specification and constructor documentation for further 
    information about function arguments. 
    """
    if u'mod_attrs' in d :
      mod_attrs = d.pop(u'mod_attrs')
    else :
      mod_attrs = Datum.getFieldsAll()

    if len(t) == 0:  # tous les arg sont nommés
      self.upgrade(d, mod_attrs=mod_attrs)
    else:
      for elem in t :
        if isinstance(elem, self._datum_type) :
          # t = [Datum, Datum, ...]
          self.upgrade({elem.name:elem}, mod_attrs=mod_attrs)
        elif isinstance(elem, Container) :
          self.upgrade(elem, mod_attrs=mod_attrs)
        elif type(elem) == dict :
          # t = {'a':1}, {'x':3, 'y':4}
          self.upgrade(elem, mod_attrs=mod_attrs)

  def walk(self, pattern=u'*'):
    return self.recurse(pattern, u'')

  def walkDatum(self, pattern=u'*'):
    return self.recurse(pattern, u'', mode=u'datum')

  def recurse(self, pattern, parent, mode=u'all'):
    u"""
    expert only. You should use walkDatum and walk instead.
    """
    if mode == u'all':
      yield parent
    for obj in self.values():
      if parent :
        path = parent + u'/' + obj.name
      else :
        path = obj.name
      if isinstance(obj, Container):
        for subpath in obj.recurse(pattern, path, mode=mode):
          yield subpath
      else :
        yield path

  def userUpgrade(self, book):
    self.upgrade(book,
      mod_attrs=[u'value', u'default_value', u'value_list'],
      only_existing_keys=True)

  def upFromKeyValueDict(self, udict):
    for c_id, c in self.iterContainerItems():
      if c_id in udict :
        c.upFromKeyValueDict(udict[c_id])
    for d_id, d in self.iterDatumItems():
      if d_id in udict :
        value = udict[d_id]
        if value.strip() == u'None':
          value = None
        d.value = value

  def upgrade (self, book, mod_attrs=None, strict=True,
               only_existing_keys=False):
    u"""
    Upgrades do a Datum update for all Datum yet in Container
    Else add datum

    :param book: the updater
    :type book: Container or dict of Datum or dict of value
    """
    check_type(u'book', book, [dict, Container])
    check_type(u'mod_attrs', mod_attrs, [list, tuple], none=True)
    check_type(u'only_existing_keys', only_existing_keys, bool)
    check_type(u'strict', strict, bool)

    if isinstance(book, Container):
      pass
    elif isinstance(book, dict):
      book = self.__class__(data=book.values())

    for key in self.ordered_keys :
      setattr(self, key, getattr(book, key))

    for key, value in book.iteritems() :
      if isinstance(value, Container):
        # Container: lowest level class, should be changed by IContainer
        # implementation check
        if value.name in self.keys():
          self[value.name].upgrade(value, mod_attrs=mod_attrs, strict=strict,
                                   only_existing_keys=only_existing_keys)
        else :
          self.add(value)
      else :
        if isinstance(value, self._datum_type) :
          datum = value
          if strict :  # raise error if datum nature differ from book nature
            self._checkDatumNature(datum)
          else :  # if book.nature is defined, try to convert datum to this nature
            if self.nature :
              datum.nature = self.nature
        elif isinstance(key, unicode) :
          datum = self._datum_type(key, self.nature, value=value)
        else :
          check_type(u'book', book, [self._datum_type, Container])

        if key in self.keys() :
          self[key].update(datum, mod_attrs)
        elif only_existing_keys is False :
          self[key] = datum
        else :
          pass

  def findall(self, key=None, **d):
    newbook = self.extract(key, **d)
    for path in self.extract(key, **d).walkDatum() :
      yield newbook[path]

  def extract(self, key=None, **d):
    u"""
    Extracts from Container all Datum whose field attribute matches value

    Example :

    >>> from pyLot.core import Datum, Path, Container
    >>> book = Container()
    >>> book.upFromDatum(Datum(u'PATH1', Path, u'/usr/bin', group=u'pathgroup'))
    >>> book.upFromDatum(Datum(u'INT', int, 1, 0))
    >>> book.upFromDatum(Datum(u'INT2', int, 1))
    >>> book.upFromDatum(Datum(u'PREFIX', Path, u'/usr/local', group=u'pathgroup'))
    >>> book[u'subbook/entier3'] = Datum(u'entier3', int, 3)
    >>> book[u'subbook/float1'] = Datum(u'float1', float, 3.14)
    >>> newbook = book.extract(nature=int)
    >>> lst = newbook.keys() ; lst.sort() ; lst
    [u'INT', u'INT2', u'subbook']
    >>> lst = newbook['subbook'].keys() ; lst.sort() ; lst
    [u'entier3']
    >>> lst = book.extract(group=u'pathgroup').keys() ; lst.sort() ; lst
    [u'PATH1', u'PREFIX']

    :param d: field=value. Ex: group='gr1'

    :return: a Container with Datum satisfying datum.group==group
    :rtype: Container

    """
    if key :
      d['name'] = key
    field = None
    for _key in d :
      if _key in self._datum_type.ordered_keys :
        field = _key
        value = d[_key]
        break

    if field :
      book = Container(name=self.name)
      for obj_id in self.keys():
        obj = self[obj_id]
        if isinstance(obj, Datum):
          if getattr (obj, field) == value :
            book.addDatum(obj)
        elif isinstance(obj, Container) :
          newbook = obj.extract(key, **d)
          if newbook.keys() :
            book.addContainer(newbook)
    else:
      book = self.copy()
    return book

  def hasEmptyFields(self):
    for val in self.values():
      if isinstance(val, Container):
        has_empty_fields = val.hasEmptyFields()
      else :
        has_empty_fields = val.value is None

      if has_empty_fields :
        return has_empty_fields
    return False

  def listEmptyFieldPaths(self):
    empty_field_paths = []

    for path in self.walkDatum():
      if self[path].value is None :
        empty_field_paths.append(path)

    return empty_field_paths

  def checkNoEmptyFields(self):
    empty_fields = self.listEmptyFieldPaths()
    if empty_fields :
      error = ErrContainerFieldsEmpty()
      error.message = u''.join([u"Tous les champs doivent être définis"])
      error.desc = u''.join([u'Les champs suivants ne sont pas définis :'])
      for field_path in empty_fields :
        paths = []
        elems = field_path.split(u'/')
        for i in range(len(elems)):
          path = u'/'.join(elems[:i + 1])
          paths.append(make_alias(self[path]))
        error.desc += u"\n - %s" % u' > '.join(paths)
      raise error

def container_repr(container, *args, **kwargs):
  try:
    indent = container_repr[u'indent']
  except KeyError :
    indent = 1
  space = u'  ' * indent

  keys = []
  default = container.__class__()
  args = -1
  for i, k in enumerate(container.ordered_keys) :
    v = getattr(container, k)
    if v != getattr(default, k) :
      args += 1
      if args == i :
        keys.append(u'%r' % v)
      else :
        keys.append(u'%s=%r' % (k, v))

  fields = u', '.join(keys)
  if fields :
    txt = u"%s(%s," % (type(container).__name__, fields)
  else :
    txt = u"%s(" % type(container).__name__
  if container :
    txt += u'\n%sdata=[\n' % space
    for datum_id, datum in container.iteritems():
      if isinstance(datum, Container):
        txt += u'  %s%s,\n' % (space, container_repr(datum, indent=indent + 2))
      else :
        txt += u'  %s%s,\n' % (space, datum.toKargsURepr())
    txt += u'%s]' % space
  txt += u')'
  return txt
